fix(css-extract): fix stale CSS HMR when extracted filename is hashed#14814
Open
gultyayev wants to merge 8 commits into
Open
fix(css-extract): fix stale CSS HMR when extracted filename is hashed#14814gultyayev wants to merge 8 commits into
gultyayev wants to merge 8 commits into
Conversation
editing CSS never updated the page when CssExtractRspackPlugin used a hashed filename ([contenthash]). The runtime's `miniCssF` function is baked into the runtime chunk as a string literal and is never re-evaluated by HMR, so it kept returning the build-1 filename forever. Extend the hot-update manifest with a `css` map (chunk id -> freshly emitted CSS filename) computed from the chunk's own emitted files, independent of the (unreliable) `miniCssF` runtime function. Thread it through a new `HMR_CSS_FILENAMES` runtime global (__webpack_require__.hmrCF) that the extract-css HMR handler now prefers over `miniCssF`. Locating the currently installed <link> tag also needed a fix: once the href changes, exact href matching can no longer find it. Tag runtime-created links with the chunk id (data-webpack-chunk) and match on that first; fall back to the frozen miniCssF href only for the very first update, since that is the only case where the tag was written directly into the HTML by HtmlRspackPlugin and never tagged. Add a regression e2e test with a hashed extract-css filename that asserts the computed style actually updates across two edits in a row, and that exactly one stylesheet link remains.
Keeps the hot-update manifest shape unchanged for builds that don't touch CSS, instead of always emitting an empty css object.
Regenerated via rstest -u; the only change is the new css chunk-id -> filename map added to the hot-update manifest.
…matching, and cross-app identity Three follow-up fixes from review: - Lazy chunks loaded after an HMR update still requested the stale filename: the loading, prefetch, and preload paths only ever called the (frozen) `miniCssF`, never the retained fresh mapping. They now prefer HMR_MINI_CSS_FILENAMES too, which is exactly what web-infra-dev#6869 is about (lazy components). - The hot-update manifest picked "the first chunk file ending in .css", which can select the wrong asset when a chunk carries both native and extracted CSS, or another plugin emits its own .css file into the same chunk. Match by asset ownership (ManifestAssetType::Custom("extract-css"), same tag rspack_plugin_sri already relies on) instead. Renamed the manifest field from the generic "css" to "miniCss" (and HMR_CSS_FILENAMES to HMR_MINI_CSS_FILENAMES) to keep this extract-css-specific channel distinct from any native-CSS equivalent. - The stylesheet identity attribute was scoped only to the chunk id, which two independently compiled apps sharing the same chunk id (e.g. both "main") could collide on. Namespaced it with output.uniqueName, consistent with the identity scheme chunk/module script loading already uses (load_script.rs). Adds an e2e case that edits a lazy chunk's CSS before its first import().
Regenerated via rstest -u. Extract-css cases now show "miniCss" instead of "css"; native-CSS cases (type: 'css') correctly lost the field entirely, since it's tagged by extract-css asset ownership now instead of a ".css" filename-suffix heuristic that happened to also match native CSS assets.
CssLoadingRuntimeModule::runtime_requirements() only forwarded the create-link template's weak requirements, not the basic template's own (which now includes the HMR_MINI_CSS_FILENAMES read added for lazy-chunk support). Under experiments.runtimeMode: "rspack", a weak read compiles to a bare lexical identifier rather than a safe property access, so loading an async extracted-CSS chunk with no HMR runtime module present threw a ReferenceError instead of falling back to miniCssF. Covered by a new non-HMR runtimeMode: "rspack" config test. Also fixes a pre-existing false positive this surfaced: css-extract/with- contenthash's "no full hash runtime" check used a plain substring match that incidentally matched the new hmrMCF property name (any property starting with "h" would trip it). Tightened it to a word-boundary regex.
…/CSS drop currentUpdateMiniCssFilenames only ever accumulated. Because css_loading consults it with priority over miniCssF when first loading a chunk, a lingering entry becomes a guaranteed 404 rather than mere staleness in two cases: - a chunk rebuilt without an extract-css asset (its CSS import was removed): no new miniCss entry is written but the old one persists. Such a chunk is always present in the manifest's `c` list yet absent from `miniCss` (the manifest reports miniCss iff the current chunk carries an extract-css asset), so delete any retained entry for `c` chunks not in `miniCss`. - a chunk in the manifest's `r` (removed) list: delete its retained entry too. A later first-time import() of such a chunk now resolves miniCssF (or nothing) instead of preferring a href the dev server no longer holds. Also document why the lazy-chunk e2e test waits on HMR apply completion (the repo's own hot client message) before triggering the first import(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEYqP3tNtaUW3RarNrSwLw
Contributor
Merging this PR will not alter performance
Comparing Footnotes
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
With
CssExtractRspackPlugin({ filename: '[name].[contenthash:8].css' })+ HMR, editing a CSS file never updated the page. Not the off-by-one described in #6869 — permanent, for every edit. Console reported success ("Reload all css"), a network request fired each cycle, and a fresh stylesheet link element was created and parsed, but the content never changed.Root cause:
__webpack_require__.miniCssFis a string literal baked into the runtime chunk at build time. HMR never re-fetches or re-evaluates the runtime chunk, so once the CSS filename's[contenthash]changed on rebuild, the extract-css HMR handler kept resolving the build-1 href forever. A second, compounding bug: even with a fresh href, the stylesheet element lookup (extractCssFindStylesheet) matched by exact href equality, which can never find the currently-installed element once its href has moved on.Fix:
crates/rspack_plugin_hmr: the hot-update manifest now carries an optionalminiCssmap ({chunkId: freshCssFilename}), computed directly from the chunk's own emitted extract-css asset at manifest-generation time — independent of the (unreliable)miniCssFruntime function. Matched by asset ownership (ManifestAssetType::Custom("extract-css"), the same tagrspack_plugin_srialready relies on), not by filename suffix, so a chunk carrying both native CSS and extract-css output — or another plugin's own.cssasset — can't have the wrong one picked. Exposed to the runtime via a newHMR_MINI_CSS_FILENAMESglobal (__webpack_require__.hmrMCF), only emitted when non-empty. NamedminiCss(not a genericcss) so this extract-css-specific channel stays distinct from any equivalent for the native CSS runtime.crates/rspack_plugin_extract_css: runtime-created stylesheet link elements are tagged with adata-webpack-chunkattribute (the owning chunk id, namespaced byoutput.uniqueNameso two independently compiled apps sharing a chunk id on one page can't steal or remove each other's stylesheet — the same identity scheme chunk/module script loading already uses). Lookups during HMR prefer matching on that instead of href. The one exception is the very first HMR update after a page load, where the element was written directly into the HTML byHtmlRspackPluginand was never tagged — for that case only, the frozenminiCssFhref is used as a lookup fallback (it still matches that element's real, unmodified href), while the manifest's fresh href is always used for the fetch itself.miniCssF, so importing a lazy chunk after its CSS changed (the scenario [Bug]: live reload of styles lazy components is broken when using hashes in names #6869 itself is about) resolves the current filename instead of the stale one.CssLoadingRuntimeModule::runtime_requirements()(crates/rspack_plugin_extract_css/src/runtime.rs) now also propagates the basic template's weak requirements (CSS_LOADING_BASIC_RUNTIME_REQUIREMENTS.weak, which now includes theHMR_MINI_CSS_FILENAMESread added above) alongside the create-link template's — it previously only propagated the latter. Without this, underexperiments.runtimeMode: "rspack"(lexical runtime, where a weak global read compiles to a bare identifier rather than a safe__webpack_require__.xproperty access), loading an async extracted-CSS chunk with no HMR runtime module present would throw aReferenceErrorfor the undeclaredhmrMiniCssFilenamesbinding instead of silently falling back tominiCssF. Covered by a new non-HMRruntimeMode: "rspack"config test (see below).I deliberately left
packages/rspack/src/runtime/cssExtractHmr.ts(the loader'smodule.hot.disposefallback, used whenruntime: false) unchanged. It already coexists with the nativehmrC.miniCsshandler today — proven by the pre-existingshared-stylesheet-dedupe2e test — and this fix doesn't change its behavior or introduce a new conflict between the two.While chasing the
runtimeMode: "rspack"config test above through the fullconfigCasessuite, I also found and fixed a pre-existing, unrelated false positive:css-extract/with-contenthash's "should not contain full hash runtime module" test used a plain substring check for__webpack_require__.h, which incidentally also matches any longer property name starting with the same letter — including the newhmrMCFglobal added here. Tightened the check to a word-boundary regex so it only matches the actual full-hash accessor.Related links
How it was verified
compiler.watch, no browser) confirmed the hot-update manifest now carries the correct, freshly-hashed CSS filename per rebuild.tests/e2e/cases/css/contenthash-hmr— hashed extract-css filename + HMR, asserts the computed background color actually updates across two consecutive edits (not just an off-by-one), and that exactly one stylesheet element remains after each update.tests/e2e/cases/css/contenthash-hmr-lazy-chunk— edits a lazy chunk's CSS before its firstimport(), then imports it, and asserts the loaded stylesheet reflects the edit rather than the stale pre-edit filename.tests/rspack-test/configCases/runtime/runtime-mode-css-extract-async-chunk— new config test: loads an async extracted-CSS chunk underexperiments.runtimeMode: "rspack"with no HMR involved at all, asserting it resolves and doesn't throw theReferenceErrordescribed above. Verified it actually catches the regression: fails with the exactReferenceErrorwhen the weak-requirement propagation fix is reverted, passes with it restored.configCasessuite (Config.part1-3.test.js+RuntimeModeConfig.part1-3.test.js, ~3700 tests each covering both legacy webpack and rspack lexical runtime rendering) — all passing, including thewith-contenthashfix above.pnpm run test:hot— 124/124 passing (hot-update manifest snapshots updated to reflect the newminiCssfield; diffs reviewed — extract-css cases gainedminiCss, native-CSS (type: 'css') cases correctly lost the field entirely instead of the old filename-suffix heuristic accidentally matching their asset too).pnpm run test:e2e) — all passing, including the pre-existingshared-stylesheet-dedupandshared-stylesheet-dedup-linkcases (no regression to the two-HMR-owners fix).cargo test(rspack_core, rspack_plugin_extract_css, rspack_plugin_hmr, and the full workspace excluding binding crates per contribution guide) — all green.Checklist